package se.lnu.axez;

import android.opengl.Matrix;

/**
 * 3D scene camera.
 * 
 * @author stan
 * 
 */
public final class Camera {
	/**
	 * Coefficient to convert degrees to radians.
	 */
	private static final float DEG_TO_RAD = (float) (Math.PI / 180d);
	
	/**
	 * Camera projection matrix offset.
	 */
	private static final int PROJ_MATRIX_OFFSET = 0;
	
	/**
	 * Camera model-view matrix offset.
	 */
	private static final int MV_MATRIX_OFFSET = PROJ_MATRIX_OFFSET + Constants.GL_MATRIX_SIZE;
	
	/**
	 * Factory method which creates new camera instance.
	 */
	public static Camera createNew(float fovy, float zNear, float zFar) {
		return new Camera(fovy, zNear, zFar);
	}
	
	/**
	 * Array containing projection and model-view matrices.
	 */
	private float[] matrices = new float[MV_MATRIX_OFFSET + Constants.GL_MATRIX_SIZE];
	
	private final float fovy;
	private final float zNear;
	private final float zFar;

	private int viewportX;
	private int viewportY;
	
	private int viewportWidth;
	private int viewportHeight;
	
	private Camera(float fovy, float zNear, float zFar) {
		Matrix.setIdentityM(this.matrices, PROJ_MATRIX_OFFSET);
		Matrix.setIdentityM(this.matrices, MV_MATRIX_OFFSET);
		this.fovy = fovy;
		this.zNear = zNear;
		this.zFar = zFar;
	}
	
	/**
	 * Sets camera orientation.
	 */
	public Camera lookAt(float eyeX, float eyeY, float eyeZ, float centerX, float centerY, float centerZ, float upX, float upY, float upZ) {
		Matrix.setLookAtM(this.matrices, MV_MATRIX_OFFSET, eyeX, eyeY, eyeZ, centerX, centerY, centerZ, upX, upY, upZ);
		return this;
    }

	/**
	 * Gets pick ray for touch point with coordinates x and y normalized in view
	 * coordinates.
	 */
	void getCameraCoordinates(float x, float y, float[] out, int offset) {
		float normalX = 2f * ((float)x - this.viewportX) / (float)this.viewportWidth - 1f;
		float normalY = 2f * (this.viewportHeight - (float)y - this.viewportY) / (float)this.viewportHeight - 1f;
		out[offset] = normalX;
		out[offset + 1] = normalY;
	}

	/**
	 * Initializes camera's viewport.
	 * @param x
	 * @param y
	 * @param width
	 * @param height
	 */
	void initializeViewport(int x, int y, int width, int height) {
		final float tanHalfFovy = (float) Math.tan((fovy * DEG_TO_RAD) / 2);
		this.viewportX = x;
		this.viewportY = y;
		this.viewportWidth = width;
		this.viewportHeight = height;
		float aspect = width / (float) height;

		this.matrices[PROJ_MATRIX_OFFSET + 5] = 1 / tanHalfFovy;

		// column major matrix
		this.matrices[PROJ_MATRIX_OFFSET + 0] = this.matrices[PROJ_MATRIX_OFFSET + 5] / aspect;
		this.matrices[PROJ_MATRIX_OFFSET + 1] = 0.0f;
		this.matrices[PROJ_MATRIX_OFFSET + 2] = 0.0f;
		this.matrices[PROJ_MATRIX_OFFSET + 3] = 0.0f;

		this.matrices[PROJ_MATRIX_OFFSET + 4] = 0.0f;
		// this.projectMatrix[5] = 1 / tanHalfFovy; // already set
		this.matrices[PROJ_MATRIX_OFFSET + 6] = 0.0f;
		this.matrices[PROJ_MATRIX_OFFSET + 7] = 0.0f;

		this.matrices[PROJ_MATRIX_OFFSET + 8] = 0.0f;
		this.matrices[PROJ_MATRIX_OFFSET + 9] = 0.0f;
		this.matrices[PROJ_MATRIX_OFFSET + 10] = (zFar + zNear) / (zNear - zFar);
		this.matrices[PROJ_MATRIX_OFFSET + 11] = -1.0f;

		this.matrices[PROJ_MATRIX_OFFSET + 12] = 0.0f;
		this.matrices[PROJ_MATRIX_OFFSET + 13] = 0.0f;
		this.matrices[PROJ_MATRIX_OFFSET + 14] = (2 * zFar * zNear) / (zNear - zFar);
		this.matrices[PROJ_MATRIX_OFFSET + 15] = 0.0f;
	}
	
	/**
	 * Sets camera's projection matrix to {@link RenderMatrices} instance.
	 * @param rm
	 */
	void setProjectionMatrix(RenderMatrices rm) {
		rm.setProjectionMatrix(this.matrices, PROJ_MATRIX_OFFSET);		
	}
	
	/**
	 * Sets camera's model-view matrix to {@link RenderMatrices} instance.
	 * @param rm
	 */
	void setModelViewMatrix(RenderMatrices rm) {
		rm.setModelViewMatrix(this.matrices, MV_MATRIX_OFFSET);
	}
	
	private float normalizeX(float x) {
		return (x - this.viewportX) / this.viewportWidth * 2f - 1f;
	}
	
	private float normalizeY(float y) {
		return (y - this.viewportY) / this.viewportHeight * 2f - 1f;
	}
	
	private float normalizeZ(float z) {
		return z * 2f - 1f;
	}
}
